#!/bin/ksh
# SPDX-License-Identifier: CDDL-1.0

#
# This file and its contents are supplied under the terms of the
# Common Development and Distribution License ("CDDL"), version 1.0.
# You may only use this file in accordance with the terms of version
# 1.0 of the CDDL.
#
# A full copy of the text of the CDDL should have accompanied this
# source.  A copy of the CDDL is also available via the Internet at
# http://www.illumos.org/license/CDDL.
#

#
# Copyright (c) 2021 by vStack. All rights reserved.
#

. "$STF_SUITE"/include/libtest.shlib
. "$STF_SUITE"/include/blkdev.shlib

#
# Description:
#
# Test whether zhack label repair commands can recover detached devices
# and corrupted checksums with a variety of sizes, and ensure
# the purposes of either command is cleanly separated from the others.
#
# Strategy:
#
# Tests are done on loopback devices with sizes divisible by label size and sizes that are not.
#
# Test one:
#
# 1. Create pool on a loopback device with some test data
# 2. Checksum repair should work with a valid TXG. Repeatedly write and
#    sync the pool so there are enough transactions for every uberblock
#    to have a TXG
# 3. Export the pool.
# 4. Corrupt all label checksums in the pool
# 5. Check that pool cannot be imported
# 6. Verify that it cannot be imported after using zhack label repair -u
#    to ensure that the -u option will quit on corrupted checksums.
# 7. Use zhack label repair -c on device
# 8. Check that pool can be imported and that data is intact
#
# Test two:
#
# 1. Create pool on a loopback device with some test data
# 2. Detach either device from the mirror
# 3. Export the pool
# 4. Remove the non-detached device and its backing file
# 5. Verify that the remaining detached device cannot be imported
# 6. Verify that it cannot be imported after using zhack label repair -c
#    to ensure that the -c option will not undetach a device.
# 7. Use zhack label repair -u on device
# 8. Verify that the detached device can be imported and that data is intact
#
# Test three:
#
# 1. Create pool on a loopback device with some test data
# 2. Detach either device from the mirror
# 3. Export the pool
# 4. Remove the non-detached device and its backing file
# 5. Corrupt all label checksums on the remaining device
# 6. Verify that the remaining detached device cannot be imported
# 7. Verify that it cannot be imported after using zhack label repair -u
#    to ensure that the -u option will quit on corrupted checksums.
# 8. Verify that it cannot be imported after using zhack label repair -c
#    -c should repair the checksums, but not undetach a device.
# 9. Use zhack label repair -u on device
# 10. Verify that the detached device can be imported and that data is intact
#
# Test four:
#
# 1. Create pool on a loopback device with some test data
# 2. Detach either device from the mirror
# 3. Export the pool
# 4. Remove the non-detached device and its backing file
# 5. Corrupt all label checksums on the remaining device
# 6. Verify that the remaining detached device cannot be imported
# 7. Use zhack label repair -cu on device to attempt to fix checksums and
#    undetach the device in a single operation.
# 8. Verify that the detached device can be imported and that data is intact
#

log_assert "Verify zhack label repair <operation> <vdev> will repair label checksums and uberblocks"
log_onexit cleanup

LABEL_SIZE="$((2**18))"
LABEL_NVLIST_END="$((LABEL_SIZE / 2))"
LABEL_CKSUM_SIZE="32"
LABEL_CKSUM_START="$(( LABEL_NVLIST_END - LABEL_CKSUM_SIZE ))"

VIRTUAL_DISK=$TEST_BASE_DIR/disk
VIRTUAL_MIRROR_DISK=$TEST_BASE_DIR/mirrordisk

VIRTUAL_DEVICE=
VIRTUAL_MIRROR_DEVICE=

function cleanup_lo
{
	typeset L_DEVICE="$1"

	if [[ -e $L_DEVICE ]]; then
		if is_linux; then
			log_must losetup -d "$L_DEVICE"
		elif is_freebsd; then
			log_must mdconfig -d -u "$L_DEVICE"
		else
			log_must lofiadm -d "$L_DEVICE"
		fi
	fi
}

function cleanup
{
	poolexists "$TESTPOOL" && destroy_pool "$TESTPOOL"
	cleanup_lo "$VIRTUAL_DEVICE"
	cleanup_lo "$VIRTUAL_MIRROR_DEVICE"
	VIRTUAL_DEVICE=
	VIRTUAL_MIRROR_DEVICE=
	[[ -f "$VIRTUAL_DISK" ]] && log_must rm "$VIRTUAL_DISK"
	[[ -f "$VIRTUAL_MIRROR_DISK" ]] && log_must rm "$VIRTUAL_MIRROR_DISK"
}

RAND_MAX="$((2**15 - 1))"
function get_devsize
{
	if [ "$RANDOM" -gt "$(( RAND_MAX / 2 ))" ]; then
		echo "$(( MINVDEVSIZE + RANDOM ))"
	else
		echo "$MINVDEVSIZE"
	fi
}

function pick_logop
{
	typeset L_SHOULD_SUCCEED="$1"

	typeset l_logop="log_mustnot"
	if [ "$L_SHOULD_SUCCEED" == true ]; then
		l_logop="log_must"
	fi

	echo "$l_logop"
}

function check_dataset
{
	typeset L_SHOULD_SUCCEED="$1"

	typeset L_LOGOP=
	L_LOGOP="$(pick_logop "$L_SHOULD_SUCCEED")"

	"$L_LOGOP" mounted "$TESTPOOL"/"$TESTFS"

	"$L_LOGOP" test -f "$TESTDIR"/"test"
}

function setup_dataset
{
	log_must zfs create "$TESTPOOL"/"$TESTFS"

	log_must mkdir -p "$TESTDIR"
	log_must zfs set mountpoint="$TESTDIR" "$TESTPOOL"/"$TESTFS"

	log_must mounted "$TESTPOOL"/"$TESTFS"

	log_must touch "$TESTDIR"/"test"
	log_must test -f "$TESTDIR"/"test"

	log_must zpool sync "$TESTPOOL"

	check_dataset true
}

function force_transactions
{
	typeset L_TIMES="$1"
	typeset i=
	for ((i=0; i < L_TIMES; i++))
	do
		touch "$TESTDIR"/"test" || return $?
		zpool sync -f "$TESTPOOL" || return $?
	done
	return 0
}

function get_practical_size
{
	typeset L_SIZE="$1"

	if [ "$((L_SIZE % LABEL_SIZE))" -ne 0 ]; then
		echo "$(((L_SIZE / LABEL_SIZE) * LABEL_SIZE))"
	else
		echo "$L_SIZE"
	fi
}

function corrupt_sized_label_checksum
{
	typeset L_SIZE="$1"
	typeset L_LABEL="$2"
	typeset L_DEVICE="$3"

	typeset L_PRACTICAL_SIZE=
	L_PRACTICAL_SIZE="$(get_practical_size "$L_SIZE")"

	typeset -a L_OFFSETS=("$LABEL_CKSUM_START" \
	    "$((LABEL_SIZE + LABEL_CKSUM_START))" \
		"$(((L_PRACTICAL_SIZE - LABEL_SIZE*2) + LABEL_CKSUM_START))" \
		"$(((L_PRACTICAL_SIZE - LABEL_SIZE) + LABEL_CKSUM_START))")

	dd if=/dev/urandom of="$L_DEVICE" \
	    seek="${L_OFFSETS["$L_LABEL"]}" bs=1 count="$LABEL_CKSUM_SIZE" \
	    conv=notrunc
}

function corrupt_labels
{
	typeset L_SIZE="$1"
	typeset L_DISK="$2"

	corrupt_sized_label_checksum "$L_SIZE" 0 "$L_DISK"
	corrupt_sized_label_checksum "$L_SIZE" 1 "$L_DISK"
	corrupt_sized_label_checksum "$L_SIZE" 2 "$L_DISK"
	corrupt_sized_label_checksum "$L_SIZE" 3 "$L_DISK"
}

function try_import_and_repair
{
	typeset L_REPAIR_SHOULD_SUCCEED="$1"
	typeset L_IMPORT_SHOULD_SUCCEED="$2"
	typeset L_OP="$3"
	typeset L_POOLDISK="$4"

	typeset L_REPAIR_LOGOP=
	L_REPAIR_LOGOP="$(pick_logop "$L_REPAIR_SHOULD_SUCCEED")"
	typeset L_IMPORT_LOGOP=
	L_IMPORT_LOGOP="$(pick_logop "$L_IMPORT_SHOULD_SUCCEED")"

	log_mustnot zpool import "$TESTPOOL" -d "$L_POOLDISK"

	"$L_REPAIR_LOGOP" zhack label repair "$L_OP" "$L_POOLDISK"

	"$L_IMPORT_LOGOP" zpool import "$TESTPOOL" -d "$L_POOLDISK"

	check_dataset "$L_IMPORT_SHOULD_SUCCEED"
}

function prepare_vdev
{
	typeset L_SIZE="$1"
	typeset L_BACKFILE="$2"

	typeset l_devname=
	if truncate -s "$L_SIZE" "$L_BACKFILE"; then
		if is_linux; then
			l_devname="$(losetup -f "$L_BACKFILE" --show)"
		elif is_freebsd; then
			l_devname=/dev/"$(mdconfig -a -t vnode -f "$L_BACKFILE")"
		else
			l_devname="$(lofiadm -a "$L_BACKFILE")"
		fi
	fi
	echo "$l_devname"
}

function run_test_one
{
	typeset L_SIZE="$1"

	VIRTUAL_DEVICE="$(prepare_vdev "$L_SIZE" "$VIRTUAL_DISK")"
	log_must test -e "$VIRTUAL_DEVICE"

	log_must zpool create "$TESTPOOL" "$VIRTUAL_DEVICE"

	setup_dataset

	# Force 256 extra transactions to ensure all uberblocks are assigned a TXG
	log_must force_transactions 256

	log_must zpool export "$TESTPOOL"

	corrupt_labels "$L_SIZE" "$VIRTUAL_DISK"

	try_import_and_repair false false "-u" "$VIRTUAL_DEVICE"

	try_import_and_repair true true "-c" "$VIRTUAL_DEVICE"

	cleanup

	log_pass "zhack label repair corruption test passed with a randomized size of $L_SIZE"
}

function make_mirrored_pool
{
	typeset L_SIZE="$1"

	VIRTUAL_DEVICE="$(prepare_vdev "$L_SIZE" "$VIRTUAL_DISK")"
	log_must test -e "$VIRTUAL_DEVICE"
	VIRTUAL_MIRROR_DEVICE="$(prepare_vdev "$L_SIZE" "$VIRTUAL_MIRROR_DISK")"
	log_must test -e "$VIRTUAL_MIRROR_DEVICE"

	log_must zpool create "$TESTPOOL" "$VIRTUAL_DEVICE"
	log_must zpool attach "$TESTPOOL" "$VIRTUAL_DEVICE" "$VIRTUAL_MIRROR_DEVICE"
}

function export_and_cleanup_vdisk
{
	log_must zpool export "$TESTPOOL"

	cleanup_lo "$VIRTUAL_DEVICE"

	VIRTUAL_DEVICE=

	log_must rm "$VIRTUAL_DISK"
}

function run_test_two
{
	typeset L_SIZE="$1"

	make_mirrored_pool "$L_SIZE"

	setup_dataset

	log_must zpool detach "$TESTPOOL" "$VIRTUAL_MIRROR_DEVICE"

	export_and_cleanup_vdisk

	try_import_and_repair false false "-c" "$VIRTUAL_MIRROR_DEVICE"

	try_import_and_repair true true "-u" "$VIRTUAL_MIRROR_DEVICE"

	cleanup

	log_pass "zhack label repair detached test passed with a randomized size of $L_SIZE"
}

function run_test_three
{
	typeset L_SIZE="$1"

	make_mirrored_pool "$L_SIZE"

	setup_dataset

	log_must zpool detach "$TESTPOOL" "$VIRTUAL_MIRROR_DEVICE"

	export_and_cleanup_vdisk

	corrupt_labels "$L_SIZE" "$VIRTUAL_MIRROR_DISK"

	try_import_and_repair false false "-u" "$VIRTUAL_MIRROR_DEVICE"

	try_import_and_repair true false "-c" "$VIRTUAL_MIRROR_DEVICE"

	try_import_and_repair true true "-u" "$VIRTUAL_MIRROR_DEVICE"

	cleanup

	log_pass "zhack label repair corruption and detached test passed with a randomized size of $L_SIZE"
}

function run_test_four
{
	typeset L_SIZE="$1"

	make_mirrored_pool "$L_SIZE"

	setup_dataset

	log_must zpool detach "$TESTPOOL" "$VIRTUAL_MIRROR_DEVICE"

	export_and_cleanup_vdisk

	corrupt_labels "$L_SIZE" "$VIRTUAL_MIRROR_DISK"

	try_import_and_repair true true "-cu" "$VIRTUAL_MIRROR_DEVICE"

	cleanup

	log_pass "zhack label repair corruption and detached single-command test passed with a randomized size of $L_SIZE."
}
